03 完整的签名模块代码 发表于 2020-01-03 | 分类于 Custom View | 一、布局123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172<?xml version="1.0" encoding="utf-8"?><LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:background="#FFF" android:orientation="vertical"> <com.xxt.xtest.AutographView android:id="@+id/autograph_view" android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1"/> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:padding="20dp" android:gravity="center_vertical" android:orientation="horizontal"> <TextView android:id="@+id/text_size_tv" android:layout_width="80dp" android:layout_height="wrap_content" android:text="Text Size: 4"/> <SeekBar android:id="@+id/seek_bar" android:layout_width="match_parent" android:layout_height="wrap_content" /> </LinearLayout> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal"> <Button android:id="@+id/clear_btn" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:text="Clear" android:background="#FFF"/> <Button android:id="@+id/undo_btn" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:text="Undo" android:background="#FFF"/> <Button android:id="@+id/redo_btn" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:text="Redo" android:background="#FFF"/> <Button android:id="@+id/color_btn" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:text="Color" android:background="#FFF"/> <Button android:id="@+id/save_btn" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:text="Save" android:background="#FFF"/> </LinearLayout></LinearLayout> 二、DemoActivity123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100public class DemoActivity extends AppCompatActivity implements View.OnClickListener { private AutographView mView; private TextView mSizeTV; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.act_demo); mView = findViewById(R.id.autograph_view); mView.setBackgroundColor(Color.TRANSPARENT); mView.setDrawingCacheEnabled(true); mSizeTV = findViewById(R.id.text_size_tv); SeekBar sizeSB = findViewById(R.id.seek_bar); sizeSB.setMax(24); sizeSB.setProgress(mView.getSize()); sizeSB.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { @Override public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { if (fromUser) { mView.setSize(progress); String prompt = "Text Size: " + progress; mSizeTV.setText(prompt); } } @Override public void onStartTrackingTouch(SeekBar seekBar) {} @Override public void onStopTrackingTouch(SeekBar seekBar) {} }); findViewById(R.id.clear_btn).setOnClickListener(this); findViewById(R.id.undo_btn).setOnClickListener(this); findViewById(R.id.redo_btn).setOnClickListener(this); findViewById(R.id.color_btn).setOnClickListener(this); findViewById(R.id.save_btn).setOnClickListener(this); } @Override public void onClick(View v) { if (mView == null) return; switch (v.getId()) { case R.id.clear_btn: mView.clear(); break; case R.id.undo_btn: mView.undo(); break; case R.id.redo_btn: mView.redo(); break; case R.id.color_btn: selectColor(); break; case R.id.save_btn: save(); break; } } private void selectColor() { new ColorPickerDialog(DemoActivity.this, new ColorPickerDialog.OnColorChangedListener() { @Override public void colorChanged(int color) { mView.setColor(color); } }, mView.getColor()).show(); } private void save() { FileOutputStream out = null; String path = Environment.getExternalStorageDirectory() + "/sign_" + System.currentTimeMillis() + ".png"; try { out = new FileOutputStream(path); boolean isSaveOk = mView.getDrawingCache().compress(CompressFormat.PNG, 100, out); if (isSaveOk) { Toast.makeText(DemoActivity.this, "保存成功:" + path, Toast.LENGTH_SHORT).show(); // 发送邮件 Intent i = new Intent(Intent.ACTION_SEND); i.setType("message/rfc822"); i.putExtra(Intent.EXTRA_EMAIL, new String[]{"xianxiaotao@iCloud.com"}); i.putExtra(Intent.EXTRA_SUBJECT, "先的签名"); i.putExtra(Intent.EXTRA_TEXT, "如附件"); i.putExtra(Intent.EXTRA_STREAM, Uri.fromFile(new File(path))); startActivity(Intent.createChooser(i, "Send email...")); } } catch (Exception e) { Log.e("DemoActivity", e.getMessage()); } finally { if (out != null) { try { out.close(); } catch (IOException e) {} } } }} 三、自定义签名 View123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130/** * 签名 */public class AutographView extends View { private Paint mPaint; // 笔画集合(字) private LinkedList<LinkedList<FPoint>> mLines; // 保存撤销的笔画,用于还原 private LinkedList<LinkedList<FPoint>> mRecycles; private boolean isTouching; public AutographView(Context context, @Nullable AttributeSet attrs) { super(context, attrs); mLines = new LinkedList<>(); mRecycles = new LinkedList<>(); mPaint = new Paint(); mPaint.setColor(Color.GREEN); mPaint.setStrokeWidth(4); isTouching = false; } @Override public boolean onTouchEvent(MotionEvent event) { // 离开屏幕 if (event.getAction() == MotionEvent.ACTION_UP) { isTouching = false; } else { LinkedList<FPoint> line; // 触摸开始,则新建 line if (!isTouching) { line = new LinkedList<>(); mLines.add(line); isTouching = true; } // 开始滑动,继续使用上一个 line else { line = mLines.getLast(); } FPoint point = new FPoint(event.getX(), event.getY()); line.add(point); postInvalidate(); } return true; } @Override protected void onDraw(Canvas canvas) { for (LinkedList<FPoint> line : mLines) { // 至少有两点才需要画出线段 if (line.size() > 1) { for (int i = 1; i < line.size(); i++) { FPoint p1 = line.get(i - 1); FPoint p2 = line.get(i); canvas.drawLine(p1.x, p1.y, p2.x, p2.y, mPaint); } } } } /** * 清除 */ public void clear() { if (mRecycles != null) mRecycles.clear(); if (mLines != null) { mLines.clear(); postInvalidate(); } } /** * 撤销 */ public void undo() { if (mLines != null && mLines.size() > 0 && mRecycles != null) { mRecycles.add(mLines.removeLast()); postInvalidate(); } } /** * 还原 */ public void redo() { if (mRecycles != null && mRecycles.size() > 0 && mLines != null) { mLines.add(mRecycles.removeLast()); postInvalidate(); } } /** * 获取字体的线条粗细 * @return int */ public int getSize() { return (int) mPaint.getStrokeWidth(); } /** * 设置字体的线条粗细 * @param size int */ public void setSize(int size) { if (size > 0) { mPaint.setStrokeWidth(size); postInvalidate(); } } public int getColor() { return mPaint.getColor(); } public void setColor(int color) { mPaint.setColor(color); } private class FPoint { public float x, y; FPoint(float x, float y) { this.x = x; this.y = y; } }} 四、自定义颜色选择器123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195public class ColorPickerDialog extends Dialog { private OnColorChangedListener mListener; private int mInitialColor; protected ColorPickerDialog(@NonNull Context context, OnColorChangedListener listener, int initialColor) { super(context); mListener = listener; mInitialColor = initialColor; } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); OnColorChangedListener l = new OnColorChangedListener() { @Override public void colorChanged(int color) { mListener.colorChanged(color); dismiss(); } }; setContentView(new ColorPickerView(getContext(), l, mInitialColor)); setTitle("Pick a Color"); } public interface OnColorChangedListener { void colorChanged(int color); } private static class ColorPickerView extends View { private Paint mPaint; private Paint mCenterPaint; private final int[] mColors; private OnColorChangedListener mListener; ColorPickerView(Context c, OnColorChangedListener l, int color) { super(c); mListener = l; mColors = new int[]{0xFFFF0000,0xFFFF00FF,0xFF0000FF, 0xFF00FFFF,0xFF00FF00,0xFFFFFF00,0xFFFF0000}; Shader shader = new SweepGradient(0, 0, mColors, null); mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mPaint.setShader(shader); mPaint.setStyle(Paint.Style.STROKE); mPaint.setStrokeWidth(110); mCenterPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mCenterPaint.setColor(color); mCenterPaint.setStrokeWidth(5); } private boolean mTrackingCenter; private boolean mHighlightCenter; private static final int CENTER_X = 300; private static final int CENTER_Y = 300; private static final int CENTER_RADIUS = 110; private static final float PI = 3.1415926f; @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { setMeasuredDimension(CENTER_X * 2, CENTER_Y * 2); } @Override protected void onDraw(Canvas canvas) { float r = CENTER_X - mPaint.getStrokeWidth() * 0.5f; canvas.translate(CENTER_X, CENTER_Y); canvas.drawOval(new RectF(-r, -r, r, r), mPaint); canvas.drawCircle(0, 0, CENTER_RADIUS, mCenterPaint); if (mTrackingCenter) { int c = mCenterPaint.getColor(); mCenterPaint.setStyle(Paint.Style.STROKE); if (mHighlightCenter) { mCenterPaint.setAlpha(0xFF); } else { mCenterPaint.setAlpha(0x80); } canvas.drawCircle(0, 0, CENTER_RADIUS + mCenterPaint.getStrokeWidth(), mCenterPaint); mCenterPaint.setStyle(Paint.Style.FILL); mCenterPaint.setColor(c); } } @Override public boolean onTouchEvent(MotionEvent event) { float x = event.getX() - CENTER_X; float y = event.getY() - CENTER_Y; boolean inCenter = Math.sqrt(x*x + y*y) <= CENTER_RADIUS; switch (event.getAction()) { case MotionEvent.ACTION_DOWN: mTrackingCenter = inCenter; if (inCenter) { mHighlightCenter = true; invalidate(); break; } case MotionEvent.ACTION_MOVE: if (mTrackingCenter) { if (mHighlightCenter != inCenter) { mHighlightCenter = inCenter; invalidate(); } } else { float angle = (float) Math.atan2(y, x); float unit = angle / (2 * PI); if (unit < 0) { unit += 1; } mCenterPaint.setColor(interpColor(mColors, unit)); invalidate(); } break; case MotionEvent.ACTION_UP: if (mTrackingCenter) { if (inCenter) { mListener.colorChanged(mCenterPaint.getColor()); } mTrackingCenter = false; invalidate(); } break; } return true; } private int floatToByte(float x) { int n = Math.round(x); return n; } private int pinToByte(int n) { if (n < 0) { n = 0; } else if (n > 255) { n = 255; } return n; } private int ave(int s, int d, float p) { return s + Math.round(p * (d - s)); } private int interpColor(int colors[], float unit) { if (unit <= 0) { return colors[0]; } if (unit >= 1) { return colors[colors.length - 1]; } float p = unit * (colors.length - 1); int i = (int) p; p -= i; int c0 = colors[i]; int c1 = colors[i+1]; int a = ave(Color.alpha(c0), Color.alpha(c1), p); int r = ave(Color.red(c0), Color.red(c1), p); int g = ave(Color.green(c0), Color.green(c1), p); int b = ave(Color.blue(c0), Color.blue(c1), p); return Color.argb(a, r, g, b); } private int rotateColor(int color, float rad) { float deg = rad * 180 / 3.1415927f; int r = Color.red(color); int g = Color.green(color); int b = Color.blue(color); ColorMatrix cm = new ColorMatrix(); ColorMatrix tmp = new ColorMatrix(); cm.setRGB2YUV(); tmp.setRotate(0, deg); cm.postConcat(tmp); tmp.setYUV2RGB(); cm.postConcat(tmp); final float[] a = cm.getArray(); int ir = floatToByte(a[0] * r + a[1] * g + a[2] * b); int ig = floatToByte(a[5] * r + a[6] * g + a[7] * b); int ib = floatToByte(a[10] * r + a[11] * g + a[12] * b); return Color.argb(Color.alpha(color), pinToByte(ir), pinToByte(ig), pinToByte(ib)); } }}